/*
 * Copyright (c) 2025 Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.helidon.service.codegen;

import java.util.List;
import java.util.Optional;
import java.util.Set;

import io.helidon.codegen.CodegenException;
import io.helidon.codegen.classmodel.ContentBuilder;
import io.helidon.common.Default;
import io.helidon.common.GenericType;
import io.helidon.common.types.Annotation;
import io.helidon.common.types.Annotations;
import io.helidon.common.types.TypeName;
import io.helidon.common.types.TypeNames;

import static io.helidon.common.types.TypeNames.GENERIC_TYPE;

/**
 * Code generation for handling of {@link io.helidon.common.Default} annotations.
 */
public final class DefaultsCodegen {
    private static final TypeName VALUE_TYPE = TypeName.create(Default.Value.class);
    private static final TypeName INT_TYPE = TypeName.create(Default.Int.class);
    private static final TypeName DOUBLE_TYPE = TypeName.create(Default.Double.class);
    private static final TypeName BOOLEAN_TYPE = TypeName.create(Default.Boolean.class);
    private static final TypeName LONG_TYPE = TypeName.create(Default.Long.class);

    private DefaultsCodegen() {
    }

    /**
     * Find a default annotation within the set of annotations on an element.
     *
     * @param annotations annotations on an element
     * @param targetType  type that is expected in the code
     * @return default annotation, or empty if no default is defined
     */
    public static Optional<DefaultCode> findDefault(Set<Annotation> annotations, TypeName targetType) {
        // Same order as in DefaultResolverService class

        Optional<Annotation> found = Annotations.findFirst(VALUE_TYPE, annotations);
        if (found.isPresent()) {
            if (targetType.equals(TypeNames.STRING)) {
                return Optional.of(new DefaultCodeImpl(false, found.get(), targetType));
            }
            return Optional.of(new DefaultCodeImpl(true, found.get(), targetType));
        }
        found = Annotations.findFirst(INT_TYPE, annotations)
                .or(() -> Annotations.findFirst(DOUBLE_TYPE, annotations))
                .or(() -> Annotations.findFirst(BOOLEAN_TYPE, annotations))
                .or(() -> Annotations.findFirst(LONG_TYPE, annotations));

        return found.map(annotation -> new DefaultCodeImpl(false, annotation, targetType));
    }

    /**
     * Code generate default handling after an optional of the correct type.
     * <p>
     * Example with fixed default value (such as {@link io.helidon.common.Default.Int}:
     * <pre>
     * headers.find(MY_HEADER)
     * .map(Header::getInt) // this is expected to be part of the already generated code
     * // will be generated by this method
     * .orElse(42)
     * </pre>
     * <p>
     * Example with mapping (when using {@link io.helidon.common.Default.Value} and a non-string type):
     * <pre>
     * headers.find(MY_HEADER)
     * .map(Header::getInt) // this is expected to be part of the already generated code
     *  // will be generated by this method
     * .orElseGet(() -> mappers.map("Default Value", GenericType.STRING, GTYPE_1, "headers"))
     * </pre>
     * - {@code mappers} - common Mappers instance, must exist in the current class, name is provided as a parameter to this
     * method
     * - {@code GTYPE_1} - genericTypeNameField provided as a parameter to this method (must be generated by caller)
     *
     * @param contentBuilder  content builder that we add the code to
     * @param defaultCode     as returned by {@link #findDefault(java.util.Set, io.helidon.common.types.TypeName)}
     * @param constantHandler handler of constants
     * @param params          additional parameters, needed depending on type of the default
     */
    public static void codegenOptional(ContentBuilder<?> contentBuilder,
                                       DefaultCode defaultCode,
                                       FieldHandler constantHandler,
                                       DefaultsParams params) {

        if (defaultCode.requiresMapper()) {
            generateMapped(contentBuilder, defaultCode, constantHandler, params);
        } else {
            generateTyped(contentBuilder, defaultCode);
        }
    }

    private static void generateMapped(ContentBuilder<?> contentBuilder,
                                       DefaultCode defaultCode,
                                       FieldHandler constantHandler,
                                       DefaultsParams params) {

        String genericTypeField = constantHandler.constant("GTYPE",
                                                           TypeName.builder(GENERIC_TYPE)
                                                                   .addTypeArgument(defaultCode.targetType().boxed())
                                                                   .build(),
                                                           defaultCode.targetType(),
                                                           content -> content
                                                                   .addContent(GENERIC_TYPE)
                                                                   .addContent(".create(")
                                                                   .addContent(defaultCode.targetType())
                                                                   .addContent(".class)"));

        // .orElseGet(() -> mappers.map("Default Value", GenericType.STRING, GTYPE_1, "headers"))
        contentBuilder.addContent(".orElseGet(() -> ")
                .addContent(params.mappersField())
                .addContent(".map(\"")
                .addContent(defaultCode.annotation().stringValues().orElseThrow().getFirst())
                .addContent("\", ")
                .addContent(GenericType.class)
                .addContent(".STRING, ")
                .addContent(genericTypeField)
                .addContent(", \"")
                .addContent(params.mapperQualifier())
                .addContent("\"))");
    }

    private static void generateTyped(ContentBuilder<?> contentBuilder, DefaultCode defaultCode) {
        /*
        .orElse(42)
        .orElse("default value")
         */
        contentBuilder.addContent(".orElse(");
        Annotation annotation = defaultCode.annotation();
        List<String> values = annotation.stringValues()
                .orElseThrow(() -> new CodegenException("Value is a mandatory property of all default annotations."
                                                                + " Yet failed for: " + annotation.typeName().fqName()));

        if (values.isEmpty()) {
            throw new CodegenException("Default value is an empty array, which is not supported for code generation. "
                                               + "Annotation: " + annotation.typeName().fqName());
        }
        String value = values.getFirst();

        if (annotation.typeName().equals(VALUE_TYPE)) {
            // string is special
            contentBuilder.addContent("\"")
                    .addContent(value)
                    .addContent("\"");
        } else {
            contentBuilder.addContent(value);
        }
        contentBuilder.addContent(")");
    }

    /**
     * Information about a default element.
     */
    public interface DefaultCode {
        /**
         * If a {@code io.helidon.common.mapper.Mappers} instance is required to process this default.
         *
         * @return if a mappers instance is needed
         */
        boolean requiresMapper();

        /**
         * The default annotation.
         *
         * @return annotation found on the element that defines a default value
         */
        Annotation annotation();

        /**
         * Type of the annotated element.
         *
         * @return type of the target for default
         */
        TypeName targetType();
    }

    private record DefaultCodeImpl(boolean requiresMapper, Annotation annotation, TypeName targetType) implements DefaultCode { }
}
